/*
	(c)2010 Martial BENOIT (z80_fr(at)yahoo(dot)com)

	FILE DESCRIPTION:
	-----------------
	The goal of this program is to provide a bridge between PLCBus and networked TCP client
	Two funtions are handled here. The first one is listening to data comming from PLCBus
	interface on /dev/plcbus or provided /dev/ttySx or /dev/ttyUSBx. Received PLCBus frame
	(raw binary data) will be checked and then broadcasted to TCP client (if conected) in
	an ASCII repesentation of each HEX value of the byte.
	Example:
	--------
	Trame: 02 06 FF F0 22 64 00 77

	Second function of this program is to receive PLCBus command issued by TCP clients onto
	PLCBus network (house powerline). The TCP client send string representing all the HEX
	value representing all the byte of the PLCBus command.
	Example:
	--------
	Received from slot 0 : len:25 ->02;05;ff;f0;22;64;00;03

	REVISION HYSTORY:
	-----------------
	2010-09-27	first functionnal release still need some better code
	2010-10-05	modify select() timeout to NULL to reduce system load

*/
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <sys/un.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <termios.h>
#include <stdarg.h>
#include <signal.h>
#include <syslog.h>
#include <getopt.h>

#define MYPORT 9999
#define BACKLOG 5
#define MAXCLIENTS 2
#define MAXDATASIZE 100


// ----------------
// Constants
// ----------------
#define true 1
#define false 0
#define PRG_NAME 	"plcbus"
#define PRG_VERSION	"0.1.0"
#define PRG_DIR		"/usr/local/bin" 				// Directory where we are when in daemon mode

// Define teleinfo
#define PLCBUS_DEVICE 	"/dev/ttyUSB0" //"/dev/teleinfo"
#define	PLCBUS_BUFSIZE	9
#define	STX 0x02
#define	ETX 0x03

/*
PC Receive mode:
RX_TX_SWITCH Register
Bit7    Bit6    Bit5        Bit4        Bit3    Bit2    Bit1    Bit0
——      R_ID_SW R_ACK_SW    R_ITSELF    R_RISC  R_SW    XXXX    XXXX
*/


enum parity_e 		{  P_NONE, 	P_EVEN, 	 P_ODD };
enum flowcntrl_e 	{ FC_NONE, 	FC_RTSCTS, FC_XONXOFF };

// Config Option
struct
{
	char port[128];
	int baud;
	enum flowcntrl_e flow;
	char *flow_str;
	enum parity_e parity;
	char *parity_str;
	int databits;
	int netport;
	int verbose;
} opts = {
	.port = PLCBUS_DEVICE,
	.baud = 9600,
	.flow = FC_NONE,
	.flow_str = "none",
	.parity = P_EVEN,
	.parity_str = "none",
	.databits = 8,
	.netport = MYPORT,
	.verbose = false,
};

// ======================================================================
// Global vars
// ======================================================================
int 	g_fd_plcbus; 					// plcbus serial handle
struct termios g_oldtermios ;           // old serial config
int		g_exit_pgm;						// indicate en of the program
// Déclaration du tableau des client connectés
int clients[MAXCLIENTS];				// Tableau des fd des clients connectés

// ======================================================================
// Global funct
// ======================================================================
void close_serial(int);

/* ======================================================================
Function: clean_exit
Purpose : exit program
Input 	: exit code
Output	: -
Comments:
====================================================================== */
void clean_exit (int exit_code)
{
	int r;

	// close serials
  if (g_fd_plcbus)
  {
		// Restore Old parameters.
  	if (  (r = tcsetattr(g_fd_plcbus, TCSAFLUSH, &g_oldtermios)) < 0 )
			fprintf(stderr, "cannot restore old parameters %s: %s", opts.port, strerror(errno));

		// then close
  	close_serial(g_fd_plcbus);
  }

	if ( exit_code == EXIT_SUCCESS)
		fprintf(stdout, "Closing PLCBus\n");

	// wait a bit for output to drain
    sleep(1);

/*	if (!opts.nolock)
		uucp_unlock();
*/
	exit(exit_code);
}

/* ======================================================================
Function: fatal
Purpose : exit program due to a fatal error
Input 	: string to write in printf format
					printf other arguments
Output	: -
Comments:
====================================================================== */
void fatal (const char *format, ...)
{
	char tmpbuff[512] = "";
	va_list args;
	int len;

	va_start(args, format);
	len = vsnprintf(tmpbuff, sizeof(tmpbuff), format, args);
	tmpbuff[sizeof(tmpbuff) - 1] = '\0';
	va_end(args);

	// Write to logfile
	openlog( PRG_NAME, LOG_PID | LOG_CONS, LOG_USER);
 	syslog(LOG_INFO, tmpbuff);
 	closelog();

	fprintf(stderr,"\r\nFATAL: %s \r\n", tmpbuff );
	fflush(stderr);

	clean_exit(EXIT_FAILURE);
}

/* ======================================================================
Function: isr_handler
Purpose : Interrupt routine Code for signal
Input 	: -
Output	: -
Comments:
====================================================================== */
void isr_handler (int signum)
{
	// Does we received CTRL-C ?
	if ( signum==SIGINT || signum==SIGTERM)
	{
		// Indicate we want to quit
		g_exit_pgm = true;

		fprintf(stdout, "Received SIGINT/SIGTERM\n");
	}
	// Our receive buffer is full
	else if (signum == SIGIO)
	{
		fprintf(stdout, "Received SIGIO\n");

	}

}

/* ======================================================================
Function: init_serial
Purpose : initialize serial port
Input 	: -
Output	: Serial Port Handle
Comments: -
====================================================================== */
int init_serial(void)
{
	int tty_fd, r ;
	struct termios  options ;

  // Ouverture de la liaison serie (Nouvelle version de config.)
  if ( (tty_fd = open(opts.port, O_RDWR | O_NOCTTY)) == -1 ) //  | O_NONBLOCK
  	fatal( "init_serial %s: %s", opts.port, strerror(errno));
  else
		fprintf( stdout, "'%s' opened.\n",opts.port);

	// Get current parameters.
  if (  (r = tcgetattr(tty_fd, &g_oldtermios)) < 0 )
		fprintf(stderr, "cannot get current parameters %s: %s",  opts.port, strerror(errno));

	// clear struct
	bzero(&options, sizeof(options));

	// No Parity with 8 bits data
	options.c_cflag = (B9600 | CS8 | CLOCAL | CREAD) ;
	options.c_iflag = IGNPAR ;
	options.c_oflag = 0;		// Pas de mode de sortie particulier (mode raw).
	options.c_lflag = 0;		// non canonique
	options.c_cc[VTIME] = 0;	/* inter-character timer unused */
	options.c_cc[VMIN]  = 0;	/* blocking read until 1 character arrives */

	tcflush(tty_fd, TCIFLUSH);

  if ( ( r = tcsetattr(tty_fd,TCSANOW,&options) < 0 ) )
		fprintf(stderr, "cannot get current parameters %s: %s",  opts.port, strerror(errno));

 	return tty_fd ;
}

/* ======================================================================
Function: close_serial
Purpose : close serial port
Input 	: Serial Port Handle
Output	: -
Comments:
====================================================================== */
void close_serial(int device)
{
  if (device)
  {
		int r;

	  	// Clean up
		if ( (r = tcflush(device, TCIOFLUSH)) < 0 )
			fatal("cannot flush before quit %s: %s", opts.port, strerror(errno));
		// restore old settings
		tcsetattr(device,TCSANOW,&g_oldtermios);

		close(device) ;
	}
}


/* ======================================================================
Function: usage
Purpose : display usage
Input 	: program name
Output	: -
Comments:
====================================================================== */
int usage( char * name)
{

	printf("%s\n", PRG_NAME );
	printf("Usage is: %s [options] <tty device>\n", PRG_NAME);
	printf("Options are:\n");
	printf("  --<v>erbose  : speak more to user\n");
	printf("  --<p>ort n   : TCP send/listen on network port n (default is 9999)\n");
	printf("  --<h>elp\n");
	printf("<?> indicates the equivalent short option.\n");
	printf("Short options are prefixed by \"-\" instead of by \"--\".\n");
	printf("Example :\n");
	printf( "plcbus /dev/plcbus\nstart plcbus to continuously send over the network the frame received on port /dev/plcbus\nreceive PLCBus command from TCP and broadcast them on powerline\n\n");
}


/* ======================================================================
Function: parse_args
Purpose : parse argument passed to the program
Input 	: -
Output	: -
Comments:
====================================================================== */
void parse_args(int argc, char *argv[])
{
	static struct option longOptions[] =
	{
		{"verbose", no_argument, 0, 'v'},
		{"port", required_argument, 0, 'p'},
		{"help", no_argument, 0, 'h'},
		{0, 0, 0, 0}
	};

	int optionIndex = 0;
	int c;

	while (1)
	{
		/* no default error messages printed. */
		opterr = 0;
		c = getopt_long(argc, argv, "vhp:",	longOptions, &optionIndex);
		if (c < 0)
			break;
		switch (c)
		{
			case 'v':
				opts.verbose = true;
				break;
			case 'p':
				opts.netport = (int) atoi(optarg);
				if (opts.netport < 1024 || opts.netport > 65534)
				{
					fprintf(stderr, "--port '%d' ignored.\n", opts.netport);
					fprintf(stderr, "--port must be between 1024 and 65534\n");
					opts.netport = MYPORT;
				}
				break;
			case 'h':
				usage(argv[0]);
				exit(EXIT_SUCCESS);
			case '?':
			default:
				fprintf(stderr, "Unrecognized option.\n");
				fprintf(stderr, "Run with '--help'.\n");
				exit(EXIT_FAILURE);
		}
	} /* while */
    if ( (argc - optind) < 1 )
    {
        fprintf(stderr, "No serial port given\n");
        exit(EXIT_FAILURE);
    }
    strncpy(opts.port, argv[optind], sizeof(opts.port) - 1);
    opts.port[sizeof(opts.port) - 1] = '\0';

/*	if (opts.daemon && opts.mode != MODE_SEND)
	{
			fprintf(stderr, "--daemon ignored.\n");
			fprintf(stderr, "--daemon must be used only in mode send\n");
			opts.daemon = false;
	}
*/
	printf("PLCBus v%s\n", PRG_VERSION);
	if (opts.verbose)
	{
		printf("-- Serial port -- \n");
		printf("port is        : %s\n", opts.port);
		printf("flowcontrol    : %s\n", opts.flow_str);
		printf("baudrate is    : %d\n", opts.baud);
		printf("parity is      : %s\n", opts.parity_str);
		printf("databits are   : %d\n", opts.databits);
		printf("-- TCP port -- \n");
		printf("TCP port is    : %d\n", opts.netport);
		printf("-- Misc -- \n");
		printf("verbose is     : %s\n", opts.verbose? "yes" : "no");
		printf("\n");
	}
}

/* ======================================================================
Function: plcbus_decode_frame
Purpose : decode PLCBus frame
Input 	: byte from PLC1141, buffer length
Output	: -
Comments:
====================================================================== */
int plcbus_decode_frame( unsigned char * rxframe, int l)
{
int i, frame_err , len, id, checksum;
char decode_buffer[30];
/*
trame RX PLCBus:
LENGTH, USER CODE, HOME_UNIT, COMMAND, DATA1, DATA2, RX_TX_SWITCH, CHECKSUM
*/
//  Vérification du checksum de la trame
//  revoie -1 si erreur
    checksum = 0;
    for ( i = 0 ; i < PLCBUS_BUFSIZE ; i ++ )
    {
        checksum += rxframe[i];
    }
    checksum = checksum & 0xFF;
    if (checksum != 0x00)
    {
        if (opts.verbose == true) fprintf(stdout, "erreur checksum PLCBus! %X\n",checksum);
        return(-1);
    }
    bzero(decode_buffer,sizeof(decode_buffer));
//    if (opts.verbose == true) fprintf(stdout, "conversion octets en chaine\n");
    sprintf(decode_buffer, "%02X;%02X;%02X;%02X;%02X;%02X;%02X;%02X;%02X\n",rxframe[0],rxframe[1],rxframe[2],rxframe[3],rxframe[4],rxframe[5],rxframe[6],rxframe[7],rxframe[8]);
//    if (opts.verbose == true) fprintf(stdout, "résultat: '%s'\n",decode_buffer);

// si checksum OK alors on continu
    for ( i = 0 ; i < MAXCLIENTS ; i ++ )
    {
        if (clients[i] != 0)    // test si client [i] existe
        {
            send(clients[i],decode_buffer,strlen(decode_buffer),MSG_NOSIGNAL);
        }
    }
return (0);
}

/* ======================================================================
Function: plcbus_send_frame
Purpose : send PLCBus frame received from TCP client
Input 	: -
Output	: -
Comments:
====================================================================== */
int plcbus_send_frame( unsigned char * txframe, int l)
{
static unsigned char tmp[PLCBUS_BUFSIZE];         // chaine tempporaire
int i, retval;
i=0;
    if (l>23) return (-1);
// si longueur OK alors on continu
    sscanf(txframe, "%2hhX %*c %2hhX %*c %2hhX %*c %2hhX %*c %2hhX %*c %2hhX %*c %2hhX %*c %2hhX",&tmp[0],&tmp[1],&tmp[2],&tmp[3],&tmp[4],&tmp[5],&tmp[6],&tmp[7]);
//    fprintf(stdout, "Trame: %02X %02X %02X %02X %02X %02X %02X %02X \n",tmp[0],tmp[1],tmp[2],tmp[3],tmp[4],tmp[5],tmp[6],tmp[7]);
    retval = write(g_fd_plcbus,tmp,8);
    if (retval < 0)
    {
        if (opts.verbose == true) fprintf(stdout, "Write: %0x\n",retval);
        return (-1);
    }
return (0);
}

/* ======================================================================
Function: plcbus main
Purpose :
Input 	: -
Output	: -
Comments:
====================================================================== */
int main(int argc, char **argv)
{
	int sockfd,new_fd,numbytes,highest = 0,i;
	char buffer[MAXDATASIZE] ;

	struct sigaction exit_action;
	unsigned char c;
	unsigned char	rx_buff[PLCBUS_BUFSIZE];
	unsigned char	tx_buff[PLCBUS_BUFSIZE];
	int		rx_idx,n,length;

	rx_idx = 0;
	g_fd_plcbus = 0;
	g_exit_pgm = false;

	bzero(rx_buff, PLCBUS_BUFSIZE);
	bzero(tx_buff, PLCBUS_BUFSIZE);

	parse_args(argc, argv);


	g_fd_plcbus = init_serial();		// initialise le port COM

	// Set up the structure to specify the exit action.
	exit_action.sa_handler = isr_handler;
	sigemptyset (&exit_action.sa_mask);
	exit_action.sa_flags = 0;
	sigaction (SIGTERM, &exit_action, NULL);
	sigaction (SIGINT, &exit_action, NULL);


	struct sockaddr_in my_addr,their_addr;
	socklen_t sin_size;
	struct timeval tv;
	fd_set readfds;

	if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
	{
	 perror("socket");
	 exit(-1);
	}
	my_addr.sin_family = AF_INET;
	my_addr.sin_port = htons(MYPORT);
	my_addr.sin_addr.s_addr = INADDR_ANY;
	bzero(&(my_addr.sin_zero), 8);

	if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr)) == -1)
	{
	  perror("bind");
	  exit(-1);
	}
	if (listen(sockfd, BACKLOG) == -1)
	{
	  perror("listen");
	  exit(-1);
	}
	bzero(clients,sizeof(clients));
	highest = sockfd ;

//----------------------------------------------------------------------------
//
//          Boucle principale
//
//----------------------------------------------------------------------------
        while ( ! g_exit_pgm )
	{
		sin_size = sizeof(struct sockaddr_in);
//		tv.tv_sec = 0;
//		tv.tv_usec = 100000;//100000;
		FD_ZERO(&readfds);	// reset la stucture &readfds
		// boucle pour ajouter les clients actifs (connectés) à la liste du SELECT
		for ( i = 0 ; i < MAXCLIENTS ; i ++ )
		{
			if ( clients[i] != 0 )		// Si actif alors ajoute au SELECT
			{
				FD_SET(clients[i],&readfds);
			}
		}
		FD_SET(g_fd_plcbus,&readfds); // ajoute le descripteur du port série pour voir les éventuels nouveaux caractères arrivés
		FD_SET(sockfd,&readfds);        // ajoute le descripteur du socket pour les éventuels nouveaux connectés

		// SELECT "écoute" les clients pendant un temps "tv" (soit 100ms)
//		if (select(highest+1, &readfds, NULL, NULL, &tv) >=0 )    // test si SELECT renvoie autre chose qu'une erreur.
		if (select(highest+1, &readfds, NULL, NULL, NULL) >=0 )    // test si SELECT renvoie autre chose qu'une erreur.
		{
			// ---------------------------------------------------------------
			//
			// Nouvel entrant?
			//
			// ---------------------------------------------------------------
			if (FD_ISSET(sockfd, &readfds))		// test si des données sont présentes pour une nouvelle connection
			{
				if ((new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &sin_size)) == -1) // tentative de connection du nouveau client
				{
					perror("accept");					// ca n'a pas marché
					continue;
				}
				for( i = 0 ; i < MAXCLIENTS ; i ++ )    // connection réussi cherche une entrée libre dans la table des clients
				{
					if ( clients[i] == 0 )
					{
						clients[i] = new_fd ;			// ajoute nouveau client dans la table des clients
						break;
					}
				}
				if ( i != MAXCLIENTS )
				{
					if ( new_fd > highest )
					{
						highest = clients[i] ;
					}
					if (opts.verbose == true) printf("Connexion received from %s (slot %i) \n",inet_ntoa(their_addr.sin_addr),i);
					send(new_fd,"Welcome ! ",10,MSG_NOSIGNAL);
				}
				else    // table de client pleine envoie message au nouveau client et ferme sa connection.
				{
					send(new_fd, "No room for you ! ",18,MSG_NOSIGNAL);
					close(new_fd);
				}
			}
			// ---------------------------------------------------------------
			//
			// 			Gestion des déjà connectés
			//
			// ---------------------------------------------------------------
			for ( i = 0 ; i < MAXCLIENTS ; i ++ )
			{
				if ( FD_ISSET(clients[i],&readfds) )	// test si client [i] à été actif
				{
					if ( (numbytes=recv(clients[i],buffer,MAXDATASIZE,0)) <= 0 ) // si nombre de d'octet reçus négatif alors déconnection
					{
						if (opts.verbose == true) printf("Connexion lost from slot %i \n",i);
						close(clients[i]);
						clients[i] = 0 ;
					}
					else		// sinon affiche les caractères reçus.
					{
						buffer[numbytes] = '\0';
						if (opts.verbose == true) printf("Received from slot %i : len:%d ->%s\n",i,numbytes,buffer);
						//if (opts.verbose == true) printf("%02X %02X %02X %02X %02X %02X %02X %02X %02X\n",buffer[0],buffer[1],buffer[2],buffer[3],buffer[4],buffer[5],buffer[6],buffer[7],buffer[8]);
						plcbus_send_frame(buffer, numbytes-2);    // émission dela trame sur le PLCBus.
					}
				}
			}
			// ---------------------------------------------------------------
			//
			// 			Gestion du port série
			//
			// ---------------------------------------------------------------
			if ( FD_ISSET(g_fd_plcbus,&readfds) )		// test si le port série à été actif
			{
				// -----------------------------------------------------------
				//
				//		Lectrure port série
				//
				// -----------------------------------------------------------
                n = read(g_fd_plcbus, &c, 1);
                if (errno == EINTR) break;
                else if ( n < 0 & errno!= EAGAIN)
                {
                    fprintf(stdout, "echec de lecture!\n");
                    fflush(stdout);
                }
                else if (n > 0)
                {
                    //if (opts.verbose == true) fprintf(stdout, "Reçu: %02X\n",c);

                    if (rx_idx == 0)
                    {
                      if (c == STX) rx_buff[rx_idx++]=c;
                    }
                    else
                    {
                        rx_buff[rx_idx++]=c;
                    }

                    if (rx_idx == PLCBUS_BUFSIZE)
                    {
                        if (opts.verbose == true)
                        {
                            fprintf(stdout, "Trame: %02X %02X %02X %02X %02X %02X %02X %02X %02X\n",rx_buff[0],rx_buff[1],rx_buff[2],rx_buff[3],rx_buff[4],rx_buff[5],rx_buff[6],rx_buff[7],rx_buff[8]);
                        }
                        plcbus_decode_frame(rx_buff, rx_idx);   // decode/broadcast la trame reçue
                        rx_idx=0;
                        bzero(rx_buff, PLCBUS_BUFSIZE);
                    }
                }
            }
		}
		else
		{
			perror("select");
			continue;
		}
	} /* while */
	return 0;
}
